package com.example.rfb.serviceloader.complier;

import com.example.rfb.serviceloader.annotation.Service;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

public class ServiceLoaderCompileProcessor extends AbstractProcessor {

    static final String MISSING_SERVICES_ERROR = "No service interfaces provided for element!";
    private Map<String, List<String>> providers = new HashMap<>();

    public ServiceLoaderCompileProcessor() {
    }

    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    public Set<String> getSupportedAnnotationTypes() {
        Set<String> names = new HashSet<>();
        names.add(Service.class.getName());
        return names;
    }

    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        try {
            return processImpl(annotations, roundEnv);
        } catch (Exception var5) {
            StringWriter writer = new StringWriter();
            var5.printStackTrace(new PrintWriter(writer));
            fatalError(writer.toString());
            return true;
        }
    }

    private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (roundEnv.processingOver()) {
            generateConfigFiles();
        } else {
            processAnnotations(annotations, roundEnv);
        }

        return true;
    }

    private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Service.class);
        log(annotations.toString());
        log(elements.toString());

        for(Element element : elements){
            TypeElement providerImplementer = (TypeElement)element;

            AnnotationMirror target = null;
            for(AnnotationMirror annotationMirror : element.getAnnotationMirrors()){

                String name = getBinaryName(annotationMirror.getAnnotationType().asElement());
                if(name.equals(Service.class.getName())){
                    target = annotationMirror;
                    break;
                }
            }

            if(target == null){
                continue;
            }

            AnnotationValue targetAnnotationValue = null;
            Map<? extends ExecutableElement, ? extends AnnotationValue> map = target.getElementValues();
            for(ExecutableElement key : map.keySet()){
                if(key.getSimpleName().toString().equals("value")){
                    targetAnnotationValue = map.get(key);
                    break;
                }
            }
            if(targetAnnotationValue == null){
                continue;
            }

            DeclaredType classTypeMirror = (DeclaredType) targetAnnotationValue.getValue();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            String interfaceName = classTypeElement.getQualifiedName().toString();

            List<String> implemeters = providers.get(interfaceName);
            if(implemeters == null){
                implemeters = new ArrayList<>();
                providers.put(interfaceName, implemeters);
            }
            implemeters.add(getBinaryName(providerImplementer));

            log(interfaceName+" "+getBinaryName(providerImplementer));
        }
    }

    private void generateConfigFiles() {
        Filer filer = processingEnv.getFiler();

        for(String providerInterface : providers.keySet()){
            String resourceFile = "META-INF/services/" + providerInterface;
            log("Working on resource file: " + resourceFile);

            try {
                TreeSet<String> allServices = new TreeSet<>();

                try {
                    FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
                    System.out.println(existingFile.toUri());
                    log("Looking for existing resource file at " + existingFile.toUri());
                    Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
                    log("Existing service entries: " + oldServices);
                    allServices.addAll(oldServices);
                } catch (IOException var9) {
                    log("Resource file did not already exist.");
                }

                Set<String> newServices = new HashSet<>(providers.get(providerInterface));
                if (allServices.containsAll(newServices)) {
                    log("No new service entries being added.");
                    return;
                }

                allServices.addAll(newServices);
                log("New service file contents: " + allServices);
                FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
                OutputStream out = fileObject.openOutputStream();
                ServicesFiles.writeServiceFile(allServices, out);
                out.close();
                log("Wrote to: " + fileObject.toUri());
            } catch (IOException var10) {
                fatalError("Unable to create " + resourceFile + ", " + var10);
                return;
            }
        }
    }

    private String getBinaryName(Element element) {
        return getBinaryNameImpl(element, element.getSimpleName().toString());
    }

    private String getBinaryNameImpl(Element element, String className) {
        Element enclosingElement = element.getEnclosingElement();
        if (enclosingElement instanceof PackageElement) {
            PackageElement pkg = (PackageElement)enclosingElement;
            return pkg.isUnnamed() ? className : pkg.getQualifiedName() + "." + className;
        } else {
            TypeElement typeElement = (TypeElement)enclosingElement;
            return getBinaryNameImpl(typeElement, typeElement.getSimpleName() + "$" + className);
        }
    }

    private void log(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
        }

    }

    private void error(String msg, Element element, AnnotationMirror annotation) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element, annotation);
    }

    private void fatalError(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: " + msg);
    }
}
